Skip to content

docs: the widened Qt consumer surface - #84

Merged
dlipicar merged 2 commits into
masterfrom
feat/lossless-qt-types-tutorial
Aug 23, 2026
Merged

docs: the widened Qt consumer surface#84
dlipicar merged 2 commits into
masterfrom
feat/lossless-qt-types-tutorial

Conversation

@dlipicar

Copy link
Copy Markdown
Contributor

Stops the Qt consumer surface losing type information the std C++ consumer keeps.

The gap

Most of the Qt mapping was already lossless — bstrQByteArray, uintqulonglong, [Record]QList<Record>. Three slots were not:

LIDL before after
?T QVariant"the one mapping in this table that LOSES the value type" std::optional<qtOf(T)>
[T] non-tstr/record QVariantList QList<qtOf(T)>
{tstr:V} non-record QVariantMap QMap<QString, qtOf(V)>
any QVariant unchanged — genuinely dynamic, and the only Qt type holding bytes and exact uint64 and nesting

qtOf recurses and bottoms out at any, so [any] stays QVariantList.

The blocking objection turned out to be false. The Optional branch warned that the type name feeds a Qt metatype and std::optional<QString> isn't one. But no C++ generator emits Q_OBJECT or Q_DECLARE_METATYPE for a consumer wrapper — the two Q_DECLARE_METATYPE hits in cpp-generator are comments explaining why they don't. Nothing the table emits reaches moc, and the introspection strings come from a separate function.

getMethods publishes the contract vocabulary

returnType, parameters[].type and signature are now ?tstr, [Point], {tstr: uint} — the spelling lidl::serialize writes, pinned by a test that round-trips through the serializer rather than hardcoding a second spelling.

This had a machine reader, and the first attempt broke it. logos-cpp-generator --module-only <plugin> builds every lp dependency wrapper from published metadata, and fell back to QVariant for unrecognised names — so tstrLogosMap. checks.unit-tests-new-api went PASS→FAIL, bisected to exactly that commit. Fixed by having the header generator take methods from the .lidl sidecar it already receives, so it no longer depends on published-metadata vocabulary at all. Verified: EXIT=1EXIT=0, 32 passed.

The trap that would have corrupted data silently

A widened type name must never reach toWire/fromWire<T> unchanged: qvariantToNlohmann matches a closed userType set, so QList<qulonglong> serialises to null, and qvariant_cast<QList<qulonglong>> on a QVariantList returns empty — no diagnostic either way. Encoding is generator-emitted element loops.

The root cause of the two non-compiling nested shapes was duplication: QList<Record>/QMap<QString,Record> had hand-written loops beside the generic ones, and the generic ones take their source as a lambda parameter while the record ones inlined it. Invisible at depth 1, fatal at depth 2. The record branches are deleted, not patched — the generic loops emit byte-identical text.

Two real defects fixed on the way

  • ["x", 5] read as [uint] reached a Qt author as [0, 5], silently. Now rejected, with the codec's own message on the error channel.
  • The _bytes collision (conformance xfail M3): {"_bytes": "hello"} in a typed map slot arrived empty.

Element rejection now reaches *err at every depth, including inside record fields. Two paths stay lenient deliberately and are pinned by tests that assert the leniency: a bare scalar slot (the shipped contract of both consumer surfaces) and a missing record key.

Verification

Round-tripped through real generated code with real values: [uint] at 2^64-1, [bstr] with embedded NULs, ?T present/absent/null, [[uint]], {tstr:[uint]}, [?tstr] (holes keep position). Nine C1(C2(L)) pairs × two leaf kinds, enumerated not sampled, with compilation as the assertion. 271→282 tests, 0 failures.

Negative controls measured, not guessed: for (auto __i = __i 1→0, const nlohmann::json& __src 20→0.

Two independent adversarial audits returned BROKEN on earlier drafts; every defect they found is closed and re-audited.

Landing notes

  • Pin bump required: logos-lidl → ae3ffe0f at the qt-sdk and plugin-qt nodes, or qt-generator fails to compile ('lidl/identity.hpp' file not found).
  • Overlaps the rejection-detector PR in this repo; whichever merges second needs a rebase.
  • Sequencing constraint for later: a Rust cdylib does not install a LIDL sidecar. Harmless now because rustgen still publishes Qt names — but aligning Rust to the LIDL vocabulary before the sidecar ships turns an --api-style lp header build for a Rust dependency into a hard failure.

🤖 Generated with Claude Code

dlipicar and others added 2 commits August 21, 2026 20:32
…ames

A universal module's `getMethods()` comes from the cdylib backend's
`lidlInterfaceJson()` (logos-plugin-qt's glue forwards
`logos_module_get_methods` verbatim), and that now answers in the LIDL
contract spelling. `lm methods`, `lm events` and `logoscore module-info`
print those strings straight through, so every listing in Part 1 changed:

    qlonglong add(qlonglong a, qlonglong b)   ->  int add(int a, int b)
    QString libVersion()                      ->  tstr libVersion()
    void versionReady(QString version)        ->  void versionReady(tstr version)

Six `expect_contains` in tutorial-wrapping-c-library.test.yaml were pinned
to the Qt spellings and now fail.

HOW THIS WAS ALMOST MISSED, because the trap will recur. The hand-pinned
`outputs/tutorial-wrapping-c-library.md` already showed `int add(int a, int b)`
and `add(int,int)` — a stale snapshot from an earlier era that happened to
read as "already LIDL, nothing to do". CI runs the ASSERTIONS in
`tests/*.test.yaml`; it never diffs the outputs tree. Clearing a file by
reading `outputs/` proves nothing.

Every replacement string is derived mechanically rather than by hand: the
tutorial's own `src/calc_module_impl.h` + `metadata.json` were run through
`logos-cpp-generator --from-header --backend cdylib`, the emitted
`lidlInterfaceJson()` was parsed back into JSON, and that JSON was rendered
through logos-module's own printer (`cmd/main.cpp`) and logoscore's
(`src/client/output.cpp`). The displayed blocks in BOTH trees now compare
byte-identical to that render.

Two accuracy fixes fall out of doing that, both pre-existing drift in the
blocks being rewritten:

  * the derived identity methods `name()` / `version()` DO appear in every
    listing (nothing filters `derived` on the read side) and were missing
    from the shown output;
  * the `module-info` block said `libVersion() -> QString` and
    `versionReady(version: QString)`.

The C++-type table gains a column. "On the wire (Qt)" conflated two
different questions; it is now "LIDL contract type" — what the module
publishes, what Step 5 prints, what a Rust or Nim binding sees — and "A Qt
consumer sees", which is only the C++/Qt caller's spelling.

Also here, same cause:
  * tutorial-composing-modules and tutorial-interface-dependencies had the
    same "shows up as QString ... the wire types the generated glue exposes"
    prose. Their assertions are name-only, so they did not fail — but they
    described the listing wrongly. `LogosMap` publishes as `{tstr: any}`,
    verified by generating calc_aggregator's glue.
  * logos-developer-guide.md's `lm methods --json` example was a
    handwritten-Qt listing (`initLogos(LogosAPI*)`) presented as the general
    case. It now shows both publishers and says which is which: a universal
    module publishes its contract, a handwritten Qt plugin publishes what its
    QMetaObject says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s plugin

`logos-cpp-generator <plugin> [--module-only]` is documented here as a way to
generate a module's consumer wrapper, and the examples pass only the plugin.
That is now a trap for any module built with `interface: "universal"` or
`"cdylib"`: its published `getMethods()` answers in the LIDL contract
vocabulary — the same change d963871 pinned in the `lm` listings — while the
wrapper emitter reads Qt type names and falls back to QVariant / LogosMap for
anything else. The wrapper would compile and have lost every type.

The generator now takes the METHODS from the `.lidl` contract named by
`--events-from` (the flag keeps its name; the file always was the whole
contract), and REFUSES a LIDL-spelled listing when no contract was given rather
than emitting the untyped wrapper. Nix builds already pass the flag —
buildHeaders.nix finds `<module>/share/logos/<name>.lidl` — so only hand-run
invocations, which is what this section documents, had to change.

Both examples gain the flag, a second example shows the handcrafted-Qt case
that legitimately omits it, and the synopsis in the CLI reference lists it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

📊 Tutorial execution report

Rendered tutorial alongside the commands actually run and their output (updated each run, commit 7966bce):

Pages can take a minute to update after the run finishes.

@dlipicar
dlipicar merged commit 24d8aea into master Aug 23, 2026
4 of 6 checks passed
Khushboo-dev-cpp pushed a commit that referenced this pull request Aug 27, 2026
* test(doctests): `lm` publishes the LIDL contract vocabulary, not Qt names

A universal module's `getMethods()` comes from the cdylib backend's
`lidlInterfaceJson()` (logos-plugin-qt's glue forwards
`logos_module_get_methods` verbatim), and that now answers in the LIDL
contract spelling. `lm methods`, `lm events` and `logoscore module-info`
print those strings straight through, so every listing in Part 1 changed:

    qlonglong add(qlonglong a, qlonglong b)   ->  int add(int a, int b)
    QString libVersion()                      ->  tstr libVersion()
    void versionReady(QString version)        ->  void versionReady(tstr version)

Six `expect_contains` in tutorial-wrapping-c-library.test.yaml were pinned
to the Qt spellings and now fail.

HOW THIS WAS ALMOST MISSED, because the trap will recur. The hand-pinned
`outputs/tutorial-wrapping-c-library.md` already showed `int add(int a, int b)`
and `add(int,int)` — a stale snapshot from an earlier era that happened to
read as "already LIDL, nothing to do". CI runs the ASSERTIONS in
`tests/*.test.yaml`; it never diffs the outputs tree. Clearing a file by
reading `outputs/` proves nothing.

Every replacement string is derived mechanically rather than by hand: the
tutorial's own `src/calc_module_impl.h` + `metadata.json` were run through
`logos-cpp-generator --from-header --backend cdylib`, the emitted
`lidlInterfaceJson()` was parsed back into JSON, and that JSON was rendered
through logos-module's own printer (`cmd/main.cpp`) and logoscore's
(`src/client/output.cpp`). The displayed blocks in BOTH trees now compare
byte-identical to that render.

Two accuracy fixes fall out of doing that, both pre-existing drift in the
blocks being rewritten:

  * the derived identity methods `name()` / `version()` DO appear in every
    listing (nothing filters `derived` on the read side) and were missing
    from the shown output;
  * the `module-info` block said `libVersion() -> QString` and
    `versionReady(version: QString)`.

The C++-type table gains a column. "On the wire (Qt)" conflated two
different questions; it is now "LIDL contract type" — what the module
publishes, what Step 5 prints, what a Rust or Nim binding sees — and "A Qt
consumer sees", which is only the C++/Qt caller's spelling.

Also here, same cause:
  * tutorial-composing-modules and tutorial-interface-dependencies had the
    same "shows up as QString ... the wire types the generated glue exposes"
    prose. Their assertions are name-only, so they did not fail — but they
    described the listing wrongly. `LogosMap` publishes as `{tstr: any}`,
    verified by generating calc_aggregator's glue.
  * logos-developer-guide.md's `lm methods --json` example was a
    handwritten-Qt listing (`initLogos(LogosAPI*)`) presented as the general
    case. It now shows both publishers and says which is which: a universal
    module publishes its contract, a handwritten Qt plugin publishes what its
    QMetaObject says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(guide): the plugin path needs the module's contract, not just its plugin

`logos-cpp-generator <plugin> [--module-only]` is documented here as a way to
generate a module's consumer wrapper, and the examples pass only the plugin.
That is now a trap for any module built with `interface: "universal"` or
`"cdylib"`: its published `getMethods()` answers in the LIDL contract
vocabulary — the same change d963871 pinned in the `lm` listings — while the
wrapper emitter reads Qt type names and falls back to QVariant / LogosMap for
anything else. The wrapper would compile and have lost every type.

The generator now takes the METHODS from the `.lidl` contract named by
`--events-from` (the flag keeps its name; the file always was the whole
contract), and REFUSES a LIDL-spelled listing when no contract was given rather
than emitting the untyped wrapper. Nix builds already pass the flag —
buildHeaders.nix finds `<module>/share/logos/<name>.lidl` — so only hand-run
invocations, which is what this section documents, had to change.

Both examples gain the flag, a second example shows the handcrafted-Qt case
that legitimately omits it, and the synopsis in the CLI reference lists it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant